iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0
自我挑戰組

30天用Python打造你的數位金融實力:從零開始的FinTech入門筆記系列 第 28

用 Python 做資料遮罩、卡號 Luhn 驗證與匿名化 ID

  • 分享至 

  • xImage
  •  

今天要達成什麼?

  • 把敏感欄位做可讀但不可逆推的部分遮罩(masking)
  • 產生匿名化 ID(以鹽化 SHA-256 做穩定的假名化識別子)
  • 進行基本欄位檢核:Email 正則、信用卡 Luhn、電話位數
  • 匯出乾淨可分享的 customers_sanitized.csv,並列印問題統計

說明:本文為教學示例,不代表法遵建議;實務需遵從你的公司/地區之資安與隱私規範(如個資法/GDPR 等)。

Python 實作

import csv, re, os, hashlib

SALT = "demo_salt_2025"  # 真實專案請放到環境變數,不要硬編碼

# --- Luhn 驗證(信用卡檢核) ---
def luhn_check(number: str) -> bool:
    s = ''.join(ch for ch in number if ch.isdigit())
    if not s:
        return False
    total = 0
    rev = s[::-1]
    for i, ch in enumerate(rev):
        d = int(ch)
        if i % 2 == 1:        # 偶數位數字加倍(從右數起)
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return total % 10 == 0

# --- 基礎格式檢查 ---
EMAIL_RE = re.compile(r'^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')

# --- 遮罩函式(只保留必要可讀資訊) ---
def mask_email(e: str) -> str:
    if "@" not in e:
        return "***"
    name, domain = e.split("@", 1)
    if len(name) <= 2:
        name_mask = name[0] + "*"
    else:
        name_mask = name[0] + "*" * (len(name) - 2) + name[-1]
    return name_mask + "@" + domain

def mask_phone(p: str) -> str:
    digits = ''.join(ch for ch in p if ch.isdigit())
    if len(digits) < 4:
        return "***"
    return "*" * (len(digits) - 4) + digits[-4:]

def mask_card(c: str) -> str:
    digits = ''.join(ch for ch in c if ch.isdigit())
    if len(digits) < 10:      # 太短就不顯示
        return "***"
    return digits[:6] + "*" * (len(digits) - 10) + digits[-4:]

def pseudo_id(email_or_key: str) -> str:
    """用鹽化 SHA-256 產生穩定的假名化 ID(不可反推)"""
    h = hashlib.sha256((SALT + (email_or_key or "")).lower().encode()).hexdigest()
    return "cust_" + h[:12]

# --- 準備輸入示例檔(若不存在就建立) ---
IN_CSV = "customers.csv"
if not os.path.exists(IN_CSV):
    with open(IN_CSV, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow(["name","email","phone","card_number"])
        w.writerow(["王小明","ming.wang@example.com","0912-345-678","4111 1111 1111 1111"])
        w.writerow(["陳小姐","chenny@example","02-8765-4321","3566-0020-0000-0410"])
        w.writerow(["李O爸","li_dad@mail.com","+886-987-000-111","1234-5678-0000-0000"])
    print("已建立示範 customers.csv")

OUT_CSV = "customers_sanitized.csv"
issues_counter = {"email_invalid": 0, "card_invalid": 0, "phone_short": 0}

with open(IN_CSV, encoding="utf-8") as f_in, open(OUT_CSV, "w", newline="", encoding="utf-8") as f_out:
    r = csv.DictReader(f_in)
    fields = ["cust_id","name_masked","email_masked","phone_masked","card_masked",
              "is_email_valid","is_card_valid","issues"]
    w = csv.DictWriter(f_out, fieldnames=fields)
    w.writeheader()

    for row in r:
        name  = row.get("name","").strip()
        email = row.get("email","").strip()
        phone = row.get("phone","").strip()
        card  = row.get("card_number","").strip()

        # 驗證
        valid_email = bool(EMAIL_RE.match(email))
        valid_card  = luhn_check(card)
        phone_digits = ''.join(ch for ch in phone if ch.isdigit())

        issue_list = []
        if not valid_email:
            issues_counter["email_invalid"] += 1
            issue_list.append("email")
        if not valid_card:
            issues_counter["card_invalid"] += 1
            issue_list.append("card")
        if len(phone_digits) < 9:
            issues_counter["phone_short"] += 1
            issue_list.append("phone")

        # 匿名與遮罩
        cust_id = pseudo_id(email or name)
        name_masked = name[:1] + "*" * max(len(name) - 1, 1)

        w.writerow({
            "cust_id": cust_id,
            "name_masked": name_masked,
            "email_masked": mask_email(email),
            "phone_masked": mask_phone(phone),
            "card_masked": mask_card(card),
            "is_email_valid": int(valid_email),
            "is_card_valid": int(valid_card),
            "issues": ",".join(issue_list) if issue_list else ""
        })

print("✅ 已輸出遮罩報表:", OUT_CSV)
print("問題統計:", issues_counter)

你會看到什麼?

  • 產生或讀取 customers.csv → 寫出 customers_sanitized.csv
  • 每列包含:cust_id(鹽化雜湊)、遮罩後的姓名/Email/電話/卡號、驗證結果與問題欄位
  • 終端機會印出統計:例如 {'email_invalid': 1, 'card_invalid': 1, 'phone_short': 1}

為什麼這樣設計?

  • 最小必要揭露:報表用遮罩資訊即可溝通,不需明碼個資。
  • 穩定假名化:以 Email(或其他鍵)+SALT 生成 cust_id,在不同資料表仍能關聯,但外部無法反推身分。
  • 快速檢核:先做 Email/Luhn/電話長度等「第一層清洗」,之後再交給更嚴格的法遵與系統。

延伸練習

  1. 把 SALT 放進環境變數(例如 KYC_SALT),避免硬編碼。
  2. 加入「生日合理性」檢核(不可晚於今天;年齡 0–120 範圍)。
  3. 產出一份「問題統計報表」CSV,或用 matplotlib 畫出長條圖(類型 vs 件數)。
  4. 與 Day25 的「自動寄信」結合:每週自動寄一份去識別化報表給資料治理小組。

上一篇
生成式 AI 思維在金融:用 Python 做財經摘要 + 情緒偵測
下一篇
把模組串成 Demo:用 Streamlit 做一個「FinTech 迷你儀表板」
系列文
30天用Python打造你的數位金融實力:從零開始的FinTech入門筆記29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言